All right.
So let's go ahead and get started scripting the Obi generation for our project.
Now since different players are going to be at different areas in the Obi, I want to go ahead and generate
the Obi on the client side, but have every single player in the server generate the same Obi.
This means we're going to have to generate a seed for each server that every client is going to use
to randomly generate their Obi, and we're going to store this randomly generated seed inside of that
number value inside of replicated storage.
So all of the players in our game can access this seed and then set up their random number generators
using this seed.
So every single player is going to generate the same Obi.
So we're going to have to generate this random number on the server.
So we can create a new module script inside of server script service to act as our service.
And I'm going to use a custom template that I've created which is attached to this lecture.
Just so I don't have to sit here and type out all of these comments and stuff, it makes it really easy.
And then I can use the shortcut of control shift and R to rename this.
And we're going to call this our seed service.
And then we can rename it in the workspace as well.
And all this is going to do is grab that number value inside a replicated storage and update the value
to a random number.
So we can go ahead and create a new random data type.
I'll just call this RNG.
And then we can also get a reference to that number, value and replicated storage, which is called
seed.
And we're only going to need an initialize function for this service because we're just going to set
seed dot value equal to RNG.
And we'll just get a random number.
And now we have generated the random seed for a particular server whenever this code is going to execute.
Now this means we can go ahead and create a new script and start a starter player scripts.
Inside of the handler section we'll create another new module and I'm going to call this my RB generation
handler.
And again I'll just paste in that exact same template I've got.
And we'll rename this to RB Generation handler.
Now, how this is going to work is the player's character is going to spawn here, and we're going to
check the distance of the player's character away from this point.
So we're going to set like a distance or a range that once a player gets inside of that range, then
we're going to generate more sections of the Abbey and we can set it to something like 120 or 100 studs.
So when the player is within 100 studs of this first part, we're going to generate the first Abbey
model.
And then we're going to update that position to the end of that Abbey model.
And if the player is within 100 studs of the end of that Abbey model, then we'll generate the next
one and we'll check the distance from that one and so forth, until the player is outside of that range.
And when they are outside of that range, then we don't need to generate any more Abbey models.
So that means we're going to have to constantly check the player's distance from these different parts
that are going to be here or inside of our Abbey model.
So that means we're going to need the run service, right?
So we'll get the run service.
We're also going to need replicated storage to get access to our seed.
We'll also get the player service so we can get references to our player's character.
And since we're going to be generating this Abi on the client, we can add in some cool effects using
the tween service.
Now, what we could go ahead and do is get a reference to our player, which is players dot local player.
I'm going to create a table to keep track of all of the different OBE models that we've generated on
the map.
We'll just call this table on map models.
And then we want to keep track of the current or last part that we're going to continually generate
models onto.
So for example, the current part is going to be this one right here.
And then once we generate an AB model, we need to update the part to be that ending part for whatever
AB model.
So if we look in our AB models they each have that little end part.
And that's what we're going to constantly update that reference to.
So that way we're generating our jobs in the correct order.
So I'm just going to call this variable.
We can just call it last part.
And that's going to be equal to workspace dot Global Start which is the default.
And then we're also going to want to keep track of what model we're currently checkpointed at.
So when a player dies, right, we want to be able to move them to, uh, wherever they were last at.
And we need to keep track of that so we can create a variable.
I'm going to call it checkpoint model.
And for now we're just going to set it to nil.
Now we can go ahead and grab all of the Abi models in our game.
So I'll call this children.
And that's equal to replicated storage Abi models get children.
Now the problem here is that this Get Children function is going to return an array of the children.
But this array might be ordered differently between clients.
And that means when we use a random number picker to grab random Abi models, it might be different
between clients.
So we need to make sure that between all of the clients in our game, these tables are ordered correctly,
or they're ordered the same.
And the way we can do that is by using the table dot sort function.
We'll pass the table, which is children and then we need a function to compare uh, basically A and
B two instances.
And we're going to use the less than operator.
So we'll pass our function.
This will get passed a value a and b.
And we need to return a boolean from this function to let the sort function know how to sort this table.
So what we could do is we can compare a name if it's less than b dot name.
So you can actually compare the names of strings using the less than operator.
And it will return either true or false if A's name is less than B's name.
So for example, if I go on to the command line, I can print out like uh, if let's say this string
hello, let's say the string is greater than this other string of hello.
So obviously not it's going to be false because they're the same.
But maybe if I add a number in here then I hit enter.
As you can see now this string is considered greater than this string.
If I make this string have a larger number than this one.
So if I put two in here and then I hit enter.
As you can see now this string would be considered greater than this string.
And since all of the above models are named the same and they just have different numbers, that means
we'll be able to order them correctly between all of the different clients in our game.
So this is super important to do when we're grabbing all of those children.
Now, another thing to note is that each one of our models has an attribute called chance.
And this is going to determine how often we would like to generate a particular hobby model.
So the higher the number, the more likely this model is going to spawn.
And the way we're going to determine that is we'll create a table.
And based on the chance of that model, we'll insert however many references to the model.
So for example hobby model one has a chance of ten.
So that means we're going to insert ten references in a table to hobby model one.
Whereas hobby model ten is only going to have one reference.
So we'll have a higher chance of spawning this model over this model.
So to do that I'm going to create a table and we're going to call it hobby models.
And then we're going to loop through every single model that we grabbed in here.
So in children.
And what we want to do is we want to go ahead and get the chance from this model.
So that will be equal to model get attribute chance.
And just in case our model doesn't have the attribute on it, then we'll set a default of one.
And then once we get the chance from that model, we can go ahead and loop.
Through however many times the chances, and then we'll just insert inside of our models table this
particular model.
So if the chance is ten, then we're inserting ten references.
If the chance is one, then we're only inserting one reference.
Now we can go ahead and create our random number generators.
I'm going to call this first one random Hobby picker.
So this one is going to be responsible for randomly grabbing models out of our hobby models folder.
So random dot new.
And then we need to go ahead and pass our seed here.
So that's replicated storage dot c dot value.
And then I'm going to need another random number generator.
I'm just going to call this one random number picker.
And that's equal to random dot new.
And we'll pass the same seed as well.
And the reason I want to separate random number generators is that in case I need to generate any other
random numbers relating to like maybe setting the position of something randomly or whatever, then
I want to do it in a separate number picker, and I don't want to affect the one that's going to be
selecting the numbers, because when we randomly generate numbers, it's going to be in order.
So when we pass a seed and we grab the first ten randomly generated numbers, they're always going to
be the same.
However, if we influence it by picking a random number that's not related to grabbing a particular
hobby, then it might get messed up between clients.
So for example here.
If I had a for loop where I decided to print out ten random numbers and we're just going to do math
dot random.
And before we do that, we're going to set Math.random seed equal to like one.
If I do this.
Oops.
Let me actually print the value out in the console.
So if we do this there, we get our ten random numbers.
And if I do it again, as you can see, the random numbers are exactly the same as the previous.
However, if there was some random call of Math.random before this loop happened and I hit enter.
As you can see now the numbers are a bit off because we made a call previous before, the actual numbers
we wanted to randomly generate appeared.
So we want to create two separate number pickers.
This one's sole purpose is for randomly picking OBE models, right?
And now we can go ahead and create a couple constants to tell us the threshold for how far away the
player can be to generate models.
So we'll call this constant generation distance.
And we'll set it to a default of like 100 studs.
Now another thing I want to do is that once a player starts getting far away from previous models.
So as they're going through the Abbey, any models that were previously generated, we want to go ahead
and destroy so we can create another constant.
I'm going to call this destroy distance.
And we can either set it to 100 or maybe a little bit farther away.
We could do like 120 studs.
Now we can go ahead and create a couple functions.
One, we'll have to generate an RGB model.
And then we could have another one to destroy an RB model.
And then we're also going to need a function to constantly execute, to check how far away our player
is from the last generated RB model.
And we can connect that to like the heartbeat event and run service.
So we could call this function on heartbeat.
And then we'll check in here the distance of the player away from the last hobby.
Model.
So inside of the start function, what we'll do is we'll refer to the heartbeat event.
And we'll go ahead and connect our on heartbeat function to that event.
And inside of here, what we want to check is first off if the player has a character.
So if they don't have a character then we're just going to return.
Otherwise, we can check if the player's character.
We'll use the get pivot function, which will get the position of the humanoid root part.
If the position of that and we subtract that by the last part dot position, we get the magnitude.
So we'll wrap this in parentheses.
If this is greater than the generation distance.
So if the player is too far away to generate any hobby models, then we're just going to return and
not do anything.
Otherwise we can go ahead and call our generate hobby model function to generate a new hobby model on
the map.
And what we can do in here is we can go ahead and pick a random model to generate.
So we'll create a random number.
We'll call it random int.
And that's going to be equal to our random hobby picker.
And we're going to go ahead and get a random integer which is going to be between one.
And however many models are inside of the hobby models table.
And then we can go ahead and get a random number from hobby models, index it with random int, and
then we can go ahead and clone that model.
And now with this new cloned model, we want to go ahead and update the key frame of it to be to wherever
the last part was at.
So for example, in the beginning this is going to be our last part.
So we want to update the pivot of the model equal to this part.
So for example with hobby model zero here we're going to update the position of this part to be equal
to this part.
And that means we're going to update last part equal to this one.
So we can continually chain all of the different models together.
So we'll refer to our model call the pivot.
Two function and it doesn't know this is a model.
So we can like quickly denote it as a model.
So we'll call pivot two.
And the key frame is going to be equal to whatever the last part dot key frame is.
And then we also want to go ahead and just hide those parts that are inside of the model, the starting
and ending part.
So we can just refer to the start part inside of the model and set the transparency equal to one.
We'll make sure that can collide is off for it as well.
So can collide is equal to false.
And then those parts also have a decal in it which we can go ahead and delete.
So model dot start will find first child which is a decal just in case you name the decal differently.
And then we'll destroy the decal.
And then we can do this as well for the end part that is in our models as well.
We'll just copy that, paste that there and paste that there.
And now, if we want to be a little fancy with how we spawn these models in, then we can go ahead and
use something like the tween service.
And we could maybe like displace all of the parts in the model to be like far away.
And then we could tween them in like, it looks like they're materializing out of the air and then floating
into their correct positions, which would look kind of cool.
So to do that, we could loop through every single descendant inside of our model, so we'll get descendants.
And we want to do is we want to first make sure that this descendant is a base part, and we also want
to make sure it's not the start or ending part.
So if this descendants name is equal to start.
Or if the descendants name is equal to, and then we'll just return.
If the descendant is not a base part.
Then we'll return as well.
So if not is a bass part then we'll return.
And actually, since this is a for loop, we don't want to use return, we want to use continue.
My bad.
So we want to continue to the next descendant inside of our model.
But once we verified it's a part and it's not the starting or ending part, then to keep track of its
original key frame, we'll set an attribute on this descendant.
We'll call it a ridge key frame, and that's going to be equal to the descendant key frame.
And then we can also set an attribute of let's say it's transparency.
So we want to make all the parts invisible.
And then we want them to fade in as well as move into their correct position.
So we'll set an attribute on it.
We'll call it original transparency.
And that's going to be equal to the descendants transparency.
And then we can go ahead and update the keyframe of this descendant to some kind of random keyframe.
So we'll do descendant keyframe and then we'll offset it with a new vector three.
So vector three dot new.
And then this is where we're going to use our random number picker to generate some random numbers.
So like next integer we could do maybe an offset of up to eight studs away.
So either negative eight or positive eight for the x axis.
And then we'll just copy that and do it for the other axes.
So now we've set the C frame of our part somewhere random, and then we'll update the transparency to
make it invisible.
And now we can go ahead and use the tween service to go ahead and tween the transparency as well as
the keyframe for our descendant.
So we'll do a tween info dot new.
Maybe we'll tween it for like one second, and then you can really choose any type of easing style you
would like.
I'll just pick quad for this one.
And then for our goal table we want to go ahead and update the keyframe.
Equal to.
And we're going to get that original sea frame property.
So get attribute a ridge sea frame.
And then we also want to go ahead and update the transparency as well.
And it's going to be equal to the descendant get attribute um a ridge transparency.
And then we can just go ahead and play that tween.
Now this is going to be tweening.
Um.
Not in the workspace because we haven't parented to the workspace yet.
So once we start playing the tween, then we want to go ahead and immediately parent the model equal
to the workspace, and we're going to put it in the models folder.
And then once we've generated this new OB model, we can go ahead and update our last part variable
to be equal to model.
And we'll get the end part.
So that way when we generate the next OB model we're going to set it to the correct key frame.
So now we have this kind of basic system set up.
Let's go ahead and actually test out to see if it works.
So if we go and play test we should be within 100 studs of that part.
And we are getting an error because of a problem.
And that's because.
For some reason I use tween service and not tween info, so we'll fix that and let's try that one more
time.
Okay, there we go.
Just like that, because we are within 100 studs of this part.
It generated the first A.B. model since we were within 100 studs of that newly updated part.
It generated the second.
And since we were still within 100 studs of that last part, it generated a third A.B. model.
And then if we go ahead and walk closer, as you can see now, it's continuing to generate new A.B.
models, and we can go ahead and keep walking and keep walking.
And it's going to continually generate these A.B. models.
So now that we've got this basic generation system set up, we want to go ahead and start deleting these
other A.B. models behind us as we get farther and farther away.
And each time we walk through these different models, we want to go ahead and update that variable
for whatever our last checkpoint was.
So that way when the player dies, we can go ahead and move their character over to wherever their last
checkpoint was at.
But so far, our RB generation is working great.
You can see that all of the models are just kind of floating in and forming in together, so it looks
really cool.
Okay, so now what we want to do is when we get far enough away from a previous model, we want to go
ahead and destroy it.
So to keep track of that, we need to go ahead and insert any model we generate into that on map models
table.
So after we have cloned an RB model, then we can go ahead and insert this model into our on map models
table.
So that means the first instance inside of our on map models table is going to be the RB model that
will be farthest away from our player.
So what we can do is we can check if we have generated any models.
So we'll check if in the first index of our on map models table, a value exists there.
And we want to make sure that the player dot character will get the position of our player.
And we subtract that from our own map models get the first model, we'll get the end part and get its
position, and we'll check the magnitude.
Between these two vectors.
So if the player is greater than our destroyed distance away, then that means we can go ahead and destroy
this model.
So we could call our function destroy OB model, and we could pass the model here.
So it would be our on map models.
Get that model at the first index.
And then once we destroy it, we can go ahead and remove it out of our on map models table.
So we'll use table dot remove from on map models and remove the model at the first index.
Now inside of our Destroyable model, we can basically do the exact same thing that we did up here,
but in reverse.
So instead we'll tween all of the parts to go out randomly and then fade away.
And then once that's tween is completed, then we can go ahead and destroy the model.
So I'm just going to copy this for loop and paste it in here.
And we'll basically do the exact same thing where we check for whether or not it was the start or ending
part, and we'll make sure it's a base part.
And then once we do that.
We don't have to do any of this, but instead we're going to tween the C frame equal to a random C frame.
And we want to set the transparency equal to be completely transparent or the value of one.
So for the c frame here we can literally just set it to the descendant C frame.
And then offset it again by a random vector three, which we've done up here.
So I'll literally just copy this and then we'll just paste that there.
So now we're creating a tween to randomly move this part away while it fades out.
And then we play that tween.
And then once we've done that for all of the descendants in the model, we'll go ahead and delay a function
for the duration of this tween, which is one second if you want to make it longer.
Actually, let's go ahead and make it longer.
We'll do two seconds, so we'll delay a function for two seconds.
And at this point, all of the parts inside of our model should be completely transparent.
So we should be able to destroy the model.
And that's it.
We destroy the model, we fade it out, and then we remove it from our on map models table.
So if we go and play test the game this time.
And we began to walk along our, uh, generated orb.
Once we get far enough away from the first orb model, it should go ahead and fade out.
Tween out and then get destroyed.
So just like that, it faded away and now it's gone.
And that means the new updated value at the first index is going to be this model.
And once we get far enough away from that one, as you can see, it fades and disappears.
The next one will be that one fades and disappears and so on and so forth.
Now problem with this is that if I were to accidentally die and like fall off the map, as you can see,
all of our models have disappeared and now we're stuck at spawn and we can't generate any more models.
So this means we're going to have to keep track of what model we are checkpointed at.
And if that model, when we look through our on map models table, if the model at the first index in
that table is equal to our checkpoint model, then we don't want to destroy it.
What we want to go ahead and do is we want to go ahead and create a function to listen for when a checkpoint
gets touched on a model.
So we'll call this function on checkpoint.
Touched and it'll get past whatever model is going to be the checkpoint, as well as the other part
that touched that checkpoint part.
And that means when we generate a new AB model, what we want to do is we want to listen for when that
checkpoint part inside of our model gets touched.
So we'll connect a function to that and get that other part.
And then we'll call our on checkpoint touch function.
We'll pass the model as well as the part that touched our checkpoint.
And then we can go ahead and check if it was our player.
So if players get player from character, if we pass other part dot parent, if that's not equal to
our local player, then we're just going to return.
Another thing we want to go ahead and check is if this model is already equal to our checkpoint model,
then we'll just return because we don't care about it.
Otherwise we can go ahead and update the value inside of our checkpoint model equal to this new model.
So now we have this new checkpoint.
So that means before we actually go and destroy an OB model, we want to make sure that whatever model
is stored on this first index is not equal to our checkpoint.
So if we have a model here.
Then we want to also check if this model.
So on map models get it at that first index.
We want to make sure that it's not equal to our checkpoint model.
And we want to make sure the player is far enough away.
So if we have a model on the map and that model on our map is not equal to our checkpoint model and
the player is far enough away from it, then we can go ahead and destroy it.
So now if we go and play test our game.
As we go through and touch these invisible parts in our model.
The checkpoint is being updated to each of these models.
So for example, our checkpoint is currently set to this model.
So if I were to hop off and fall down, as you can see, this model previous to our checkpoint model
got destroyed.
But our checkpoint model and none of the models after our checkpoint model got destroyed.
And that's what we want.
Now, when the player dies, we need to, of course, go ahead and move the player's character over
to that checkpoint model, which we'll do in a second.
But there is another problem we want to take a look at.
And that's let's say the player never touches a checkpoint.
So for example, these three generated let's say I never touch this checkpoint part and I just fall
off the map.
Well now all those models just got destroyed and now I am screwed.
And I can't play the game because they're not going to generate anymore.
So to fix that problem, before we go and destroy an OBE model, we also want to make sure that a checkpoint
model actually exists that the player can move their character to.
So we want to make sure that we actually have a model on the map.
And we want to make sure that we have a checkpoint model, and we want to make sure that model on the
map is not equal to our checkpoint model.
And we want to make sure that the player is far enough away.
So if we match all of those different criteria, then we can go ahead and destroy the model.
So now if I play the game, even though I don't have a model set as my checkpoint, because we have
to verify if a checkpoint model actually exists.
As you can see, these different models did not get destroyed.
And now I've touched this model.
It is set to my checkpoint.
I touch this one, it's set to my checkpoint and so on and so forth as I progress along this RB.
And the ones behind me should start to disappear.
So there you go.
That one deleted itself.
And as I continue to walk along the map, they continue to destroy themselves, which is very, very
cool.
Now, another thing we want to do real quick, because you might have noticed that our RB generated
the same even though we played the game two different times, is that when we generate this value here
for our seed, we can go ahead and set the seed for this random number generator equal to, for example,
the current tick.
So the current tick will never be the same since this is constantly updating what time it is in the
future, and that guarantees that our random number generator is always going to generate a random number
for our seed, and it's never going to be the same.
Now, since our number value is limited up to three decimals.
If we would also like to use the real estate for whole numbers or integers as well.
When we generate a random number here, we can go ahead and multiply this by some big random number.
So you could just put in a random number here like that.
And that means when you go and generate your random seed, if we go ahead and look in replicated storage
and we take a look at our seed, now here's our randomly generated seed.
And we go and we walk along.
And now our map is being randomly generated, which is different from what we had last time.
And if I stop and then I play test my game once again, and then we go ahead and look at our seed this
time, as you can see, it's a completely different set of numbers.
And now we are generating or hopefully we should be generating a different map, as you can see right
here.
And we'll test that again one last time.
If we go and take a look at our seed, as you can see it's completely different.
And now we are generating a different Abi once again.
Okay, so the last thing we need to go ahead and do is we want to listen for when a new character is
added to our player, and we want to move that player's character to the last checkpoint they were at.
So inside of our initialize function, we can listen to when the player.
Has a character added to them, and we'll connect a function to this and get that character.
And actually we could go ahead and create a dedicated function for this.
We'll call it on character added.
And we'll get past the player's character here, so we'll actually connect on character added.
And then we can go ahead and check if we have a checkpoint model.
If we do not have a checkpoint model, then we'll just return.
Otherwise, what we could do is we could like tween our player across the map to the checkpoint.
So we'll get the player's route part, which is equal to their character.
And we're going to go ahead and wait for child humanoid route part, because there's no guarantee all
of the descendants of this player's character will be replicated immediately.
And then we'll go ahead and anchor the route part.
And then we'll create a tween.
On our route part.
We'll do a tween info.
New between for like three seconds.
And we'll pick an easy style of like sign.
And we want to go ahead and update the C frame of our players humanoid root part.
And that's going to be equal to the checkpoint model.
And inside of each of our models we have another part which is our respawn point.
And we're going to go ahead and set the keyframe of our player's humanoid part to that respawn point.
So checkpoint model dot respawn point dot keyframe.
And now that we have this new tween, we can go ahead and play it and then we'll wait for this tween
to complete.
So completed we'll actually connect a function to it.
And then we'll go ahead and set dot anchored equal to false on our root part.
So now if we go and play test the game.
If we start walking along the course or this abbey and we're updating.
So right now the checkpoint should be set to this model if my player were to fall off the map.
And I die now when the player's character gets added.
I should be tweened to the new checkpoint just like that.
And now we are back in our obby and we can continue along the course.
The map is continually generated in front of us, while the previous, uh, models behind us are going
to be destroyed just like that.
And we continually update the checkpoint.
And if I were to somehow die.
And fall out of the world.
It doesn't destroy those checkpoints that we're at.
And I get tweened to wherever I was last at.
Very cool.
And of course, this RB generation is working, even though we have two separate players here, because
both of these players are using the same seed for the random number generator, they're going to be
generating the same RB.
So if I have this player walk down and fill in more stuff here.
As you can see, we get a feel for what the pattern looks like.
We have two islands and then a bridge and then another two islands, and then this player should generate
the exact same thing.
So as you can see two islands, a bridge and then the other two islands.
And if I continue.
Looks like we get one of those islands that turns 90 degrees, and we get another bridge and another
island.
And this player should generate the exact same thing.
And as you can see, we get the exact same models and the exact same setup for our map.
That's because both of these players are using the same, uh, seed for their random number generator.
Now that we have the basic foundation for our project set up, we can go ahead and start adding more
features in the next lectures.
See you there.